;
;  Size-optimized ApLib decompressor by spke (ver.02 05-07/01/2020, 140 bytes)
;
;  The original Z80 decompressor for ApLib was written by Dan Weiss (Dwedit),
;  then tweaked by Francisco Javier Pena Pareja (utopian),
;  and optimized by Jaime Tejedor Gomez (Metalbrain).
;
;  This version was heavily re-optimized for size by spke.
;  (It is 16 bytes shorter and 21% faster than the 156b version by Metalbrain.)
;
;  ver.00 by spke (21/08/2018-01/09/2018, 141 bytes);
;  ver.01 by spke (spring 2019, 140(-1) byte, slightly faster);
;  ver.02 by spke (05-07/01/2020, added full revision history, support for long offsets
;                  and an option to use self-modifying code instead of IY)
;
;  The data must be compressed using any compressor for ApLib capable of generating raw data.
;  At present, two best available compressors are:
;
;  "APC" by Sven-Ake Dahl: https://github.com/svendahl/cap or
;  "apultra" by Emmanuel Marty: https://github.com/emmanuel-marty/apultra
;
;  The compression can be done as follows:
;
;  apc.exe e <sourcefile> <outfile>
;  or
;  apultra.exe <sourcefile> <outfile>
;
;  A decent compressor was written by r57shell (although it is worse than compressors above):
;  http://gendev.spritesmind.net/forum/viewtopic.php?p=32548#p32548
;  The use of the official ApLib compressor by Joergen Ibsen is not recommended.
;
;  The decompression is done in the standard way:
;
;  ld hl,FirstByteOfCompressedData
;  ld de,FirstByteOfMemoryForDecompressedData
;  call DecompressApLib
;
;  The decompressor modifies AF, AF', BC, DE, HL, IXH, IY.
;  (However, note that the option "AllowSelfmodifyingCode" removes the dependency on IY.)
;
;  Of course, ApLib compression algorithms are (c) 1998-2014 Joergen Ibsen,
;  see http://www.ibsensoftware.com/ for more information
;
;  Drop me an email if you have any comments/ideas/suggestions: zxintrospec@gmail.com
;
;  This software is provided 'as-is', without any express or implied
;  warranty.  In no event will the authors be held liable for any damages
;  arising from the use of this software.
;
;  Permission is granted to anyone to use this software for any purpose,
;  including commercial applications, and to alter it and redistribute it
;  freely, subject to the following restrictions:
;
;  1. The origin of this software must not be misrepresented; you must not
;     claim that you wrote the original software. If you use this software
;     in a product, an acknowledgment in the product documentation would be
;     appreciated but is not required.
;  2. Altered source versions must be plainly marked as such, and must not be
;     misrepresented as being the original software.
;  3. This notice may not be removed or altered from any source distribution.

;	DEFINE FasterGetBit					; 16% speed-up at the cost of extra 4 bytes
;	DEFINE AllowSelfmodifyingCode				; 1% speed-up at the cost of an extra byte (avoids using IY)
;	DEFINE SupportLongOffsets				; +4 bytes for long offset support. slows decompression down by 1%, but may be needed to decompress files >=32K

	IFDEF FasterGetBit
		MACRO	GET_BIT
			add a : call z,ReloadByte
		ENDM
	ELSE
		MACRO	GET_BIT
			call GetOneBit
		ENDM
	ENDIF

@DecompressApLib:	ld a,128

;
;  case "0"+BYTE: copy a single literal

CASE0:			ldi					; first byte is always copied as literal
ResetLWM:		ld ixh,1				; LWM = 0 (LWM stands for "Last Was Match"; a flag that we did not have a match)

;
;  main decompressor loop

MainLoop:		GET_BIT : jr nc,CASE0			; "0"+BYTE = copy literal
			GET_BIT : jr nc,CASE10			; "10"+gamma(offset/256)+BYTE+gamma(length) = the main matching mechanism

			ld bc,%11100000
			GET_BIT : jr nc,CASE110			; "110"+[oooooool] = matched 2-3 bytes with a small offset

;
;  case "111"+"oooo": copy a byte with offset -1..-15, or write zero to dest

CASE111:
ReadFourBits		GET_BIT					; read short offset (4 bits)
			rl c : jr c,ReadFourBits
			ex de,hl : jr z,WriteZero		; zero offset means "write zero" (NB: B is zero here)

			; "write a previous byte (1-15 away from dest)"
			push hl					; BC = offset, DE = src, HL = dest
			sbc hl,bc				; HL = dest-offset (SBC works because branching above ensured NC)
			ld b,(hl)
			pop hl

WriteZero		ld (hl),b : ex de,hl
			inc de : jr ResetLWM			; write one byte, reset LWM

;
;  branch "110"+[oooooool]: copy two or three bytes (bit "l") with the offset -1..-127 (bits "ooooooo"), or stop

CASE110:		; "use 7 bit offset, length = 2 or 3"
			; "if a zero is found here, it's EOF"
			ld c,(hl) : rr c : ret z		; process EOF
			inc hl

			push hl					; save src
			ld h,b : ld l,c				; HL = offset

			; flag NC means len=2, flag C means len=3
			ld c,1 : rl c : jr SaveLWMOffset
			
;
;  branch "10"+gamma(offset/256)+BYTE+gamma(length): the main matching mechanism

CASE10:			; "use a gamma code * 256 for offset, another gamma code for length"
			call GetGammaCoded

			; the original decompressor contains
			;
			; if ((LWM == 0) && (offs == 2)) { ... }
			; else {
			;	if (LWM == 0) { offs -= 3; }
			;	else { offs -= 2; }
			; }
			;
			; so, the idea here is to use the fact that GetGammaCoded returns (offset/256)+2,
			; and to split the first condition by noticing that C-1 can never be zero
			dec ixh : jr nz,NoLWM
			dec c
NoLWM			; "if gamma code is 2, use old r0 offset"
			dec c : jr z,KickInLWM
			dec c
			ld b,c : ld c,(hl) : inc hl		; BC = offset

			push bc					; (SP) = offset
			call GetGammaCoded			; BC = len*
			ex (sp),hl				; HL = offset, (SP) = src

			; interpretation of length value is offset-dependent
			exa : ld a,h
	IFDEF	SupportLongOffsets
			; NB offsets over 32000 require an additional check, which is skipped in most
			; Z80 decompressors (seemingly as a performance optimization)
			cp 32000/256 : jr nc,.Add2
	ENDIF
			cp 5 : jr nc,.Add1
			or a : jr nz,.Add0
			bit 7,l : jr nz,.Add0
.Add2			inc bc
.Add1			inc bc
.Add0			exa

SaveLWMOffset:
	IFNDEF	AllowSelfmodifyingCode
			push hl : pop iy			; save offset for future LWMs
	ELSE
			ld (KickInLWM.PrevOffset),hl
	ENDIF

CopyMatch:		; this assumes that BC = len, DE = dest, HL = offset
			; and also that (SP) = src, while having NC
			push de
			ex de,hl : sbc hl,de			; HL = dest-offset
			pop de					; DE = dest
			ldir
			pop hl					; recover src
			ld ixh,b				; LWM = 0
			jr MainLoop

;
;  the re-use of the previous offset (LWM magic)

KickInLWM:		; "and a new gamma code for length"
			call GetGammaCoded			; BC = len
			push hl
	IFNDEF	AllowSelfmodifyingCode
			push iy ;: pop hl			; DE = dest, HL = prev offset
			jr CopyMatch-1				; this jumps into POP HL of the POP IY
	ELSE
.PrevOffset		EQU $+1
			ld hl,0
			jr CopyMatch
	ENDIF

;
;  interlaced gamma code reader
;  x0 -> 1x
;  x1y0 -> 1xy
;  x1y1z0 -> 1xyz etc
;  (technically, this is a 2-based variation of Exp-Golomb-1)

GetGammaCoded:		ld bc,1
ReadGamma		GET_BIT : rl c : rl b
			GET_BIT : jr c,ReadGamma
			ret

;
;  pretty usual getbit for mixed datastreams

	IFNDEF FasterGetBit
GetOneBit:		add a : ret nz
	ENDIF
ReloadByte:		ld a,(hl) : inc hl
			rla : ret

